
package com.wobaby.util.log;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import com.wobaby.util.AssertException;
import com.wobaby.util.ExceptionSupport;
import com.wobaby.util.config.Config;

// Referenced classes of package com.wobaby.util.log:
//            LogPrinter, Log

class DefaultLogPrinter
    implements LogPrinter
{

    public void assertTrue(boolean condition, Object obj, String message)
    {
        assertTrue(condition, obj, message, null);
    }

    public void assertTrue(boolean condition, Object obj, String message, Exception except)
    {
        if(!condition)
        {
            log(90, obj, message, null);
            if(Config.getConfig().get("log/assert", "").equals("throw"))
                throw new AssertException(message, except);
        }
    }

    public void fatal(Object obj, String message)
    {
        log(99, obj, message, null);
    }

    public void fatal(Object obj, String message, Throwable except)
    {
        log(99, obj, message, except);
    }

    public void error(Object obj, String message)
    {
        log(75, obj, message, null);
    }

    public void error(Object obj, String message, Throwable except)
    {
        log(75, obj, message, except);
    }

    public void warn(Object obj, String message)
    {
        log(50, obj, message, null);
    }

    public void warn(Object obj, String message, Throwable except)
    {
        log(50, obj, message, except);
    }

    public void info(Object obj, String message)
    {
        log(25, obj, message, null);
    }

    public void info(Object obj, String message, Throwable except)
    {
        log(25, obj, message, except);
    }

    public void debug(Object obj, String message)
    {
        log(10, obj, message, null);
    }

    public void debug(Object obj, String message, Throwable except)
    {
        log(10, obj, message, except);
    }

    public void trace(Object obj, String message)
    {
        log(0, obj, message, null);
    }

    public void trace(Object obj, String message, Throwable except)
    {
        log(0, obj, message, except);
    }

    public void write(int level, Object obj, String message)
    {
        log(level, obj, message, null);
    }

    public void write(int level, Object obj, String message, Throwable except)
    {
        log(level, obj, message, except);
    }

    public void print(Object obj, String message)
    {
        log(100, obj, message, null);
    }

    public void print(Object obj, String message, Throwable except)
    {
        log(100, obj, message, except);
    }

    public void mprint(Object obj, String message)
    {
        if(obj != null)
            message = prependCallingMethodName(obj, message);
        log(100, obj, message, null);
    }

    public String getName()
    {
        return m_name;
    }

    public boolean configuredFor(int logLevel)
    {
        return m_configuredLevel >= 0 ? logLevel >= m_configuredLevel : m_configuredLevel + logLevel == 0;
    }

    DefaultLogPrinter(String name)
    {
        m_driverLoaded = false;
        m_tableCreated = false;
        m_conn = null;
        m_name = name;
        parseConfig(readConfig());
    }

    public void reload()
    {
        parseConfig(readConfig());
    }

    private String readConfig()
    {
        String logConfig = Config.getConfig().get("log/" + m_name, "");
        if(logConfig.length() == 0)
        {
            logConfig = Config.getConfig().get("log/*", "");
            if(logConfig.length() == 0)
                m_configuredOn = false;
            else
                m_configuredOn = true;
        } else
        {
            m_configuredOn = true;
        }
        if(m_configuredOn)
        {
            m_configuredLevel = Config.getConfig().get("log/" + m_name + "/level", -1);
            if(m_configuredLevel == -1)
                m_configuredLevel = Config.getConfig().get("log/*/level", 100);
        } else
        {
            m_configuredLevel = 100;
        }
        return logConfig;
    }

    private void parseConfig(String config)
    {
        if(config.startsWith("file:"))
            m_fileName = config.substring("file:".length());
        else
        if(config.startsWith("jdbc:"))
            m_dsn = config;
        else
        if(config.equals("System.out"))
            m_stream = System.out;
        else
        if(config.equals("System.err"))
            m_stream = System.err;
        else
        if(config.length() > 0)
        {
            System.out.println("Log: The log named \"" + m_name + "\" has unparsable value: >" + config + "<");
            System.out.println("Log: using System.out instead");
            System.out.println("Log: log values should start with 'file:' or be 'System.out' or 'System.err'");
        }
    }

    protected void log(int level, Object obj, String message, Throwable except)
    {
        if(configuredFor(level) || level == 100)
            if(m_fileName != null)
                logToFile(level, obj, message, except);
            else
            if(m_dsn != null)
                logToDatabase(level, obj, message, except);
            else
            if(m_stream != null)
                logToStream(level, obj, message, except);
    }

    private void logToFile(int level, Object obj, String message, Throwable except)
    {
        Date now = new Date();
        String s = formatMessage(level, obj, message, except, now);
        String expandedFileName = expandFileName(m_fileName, now);
        synchronized(this)
        {
            try
            {
                FileOutputStream fileStream = new FileOutputStream(expandedFileName, true);
                PrintStream printStream = new PrintStream(fileStream);
                printStream.print(s);
                printStream.flush();
                printStream.close();
            }
            catch(IOException e)
            {
                System.out.println("Log: Can't write log message to " + expandedFileName + ": " + e.getMessage());
            }
        }
    }

    private String formatMessage(int level, Object obj, String message, Throwable except, Date now)
    {
        StringBuffer s = new StringBuffer(50);
        DecimalFormat decfmt = new DecimalFormat("00");
        if(m_stream == null || m_testbedApplication)
        {
            SimpleDateFormat sdf = new SimpleDateFormat();
            sdf.applyPattern("MM/dd/yy HH:mm:ss.SSS");
            s.append(sdf.format(now));
            s.append(" ");
        }
        s.append("{");
        s.append(getName());
        s.append("} [");
        s.append(Thread.currentThread().getName());
        s.append("] ");
        s.append(decfmt.format(level));
        s.append(" ");
        s.append(formatClassName(obj));
        s.append(": ");
        s.append(message);
        s.append(" - ");
        s.append(formatException(except));
        s.append("\n");
        return s.toString();
    }

    private String formatClassName(Object obj)
    {
        String name = "";
        if(obj != null)
        {
            if(obj instanceof Class)
                name = ((Class)obj).getName();
            else
                name = obj.getClass().getName();
            name = name.substring(name.lastIndexOf(".") + 1);
        }
        return name;
    }

    private String formatException(Throwable ex)
    {
        if(ex == null)
            return "";
        StringBuffer sb = new StringBuffer();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        PrintWriter pw = new PrintWriter(os);
        ex.printStackTrace(pw);
        Throwable t = ExceptionSupport.findRootException(ex);
        if(t != ex)
        {
            pw.println("Root Exception:");
            t.printStackTrace(pw);
        }
        pw.flush();
        pw.close();
        sb.append(os.toString());
        return sb.toString();
    }

    private String expandFileName(String fileName, Date date)
    {
        StringBuffer name = new StringBuffer();
        SimpleDateFormat sdf = new SimpleDateFormat();
        char s[] = fileName.toCharArray();
        for(int n = 0; n < s.length; n++)
            if(s[n] == '%' && n + 1 < s.length)
                switch(s[++n])
                {
                case 110: // 'n'
                    name.append(m_name);
                    break;

                case 104: // 'h'
                    name.append(getHostName());
                    break;

                case 116: // 't'
                    name.append(Thread.currentThread().getName());
                    break;

                case 72: // 'H'
                    sdf.applyPattern("k");
                    name.append(sdf.format(date));
                    break;

                case 100: // 'd'
                    sdf.applyPattern("EEE");
                    name.append(sdf.format(date));
                    break;

                case 68: // 'D'
                    sdf.applyPattern("dd");
                    name.append(sdf.format(date));
                    break;

                case 109: // 'm'
                    sdf.applyPattern("MMM");
                    name.append(sdf.format(date));
                    break;

                case 77: // 'M'
                    sdf.applyPattern("MM");
                    name.append(sdf.format(date));
                    break;

                case 121: // 'y'
                    sdf.applyPattern("yy");
                    name.append(sdf.format(date));
                    break;

                case 89: // 'Y'
                    sdf.applyPattern("yyyy");
                    name.append(sdf.format(date));
                    break;
                }
            else
                name.append(s[n]);

        return name.toString();
    }

    private String getHostName()
    {
        try
        {
            return InetAddress.getLocalHost().getHostName();
        }
        catch(UnknownHostException e)
        {
            return "unknown";
        }
    }

    private String prependCallingMethodName(Object obj, String message)
    {
        String returnValue = message;
        Throwable t = new Throwable();
        StackTraceElement stack[] = t.getStackTrace();
        for(int i = stack.length - 1; obj != null && i >= 0; i--)
        {
            StackTraceElement e = stack[i];
            if(obj.getClass().getName().equals(e.getClassName()))
            {
                String methodName = e.getMethodName();
                returnValue = "[" + methodName + "()] " + message;
            }
        }

        return returnValue;
    }

    private void logToStream(int level, Object obj, String message, Throwable except)
    {
        String s = formatMessage(level, obj, message, except, new Date());
        m_stream.print(s);
    }

    private void logToDatabase(int level, Object obj, String message, Throwable except)
    {
        LogPrinter log = Log.getLog("jdbc");
        if(((DefaultLogPrinter)log).m_dsn != null)
        {
            ((DefaultLogPrinter)log).m_dsn = null;
            ((DefaultLogPrinter)log).m_stream = System.out;
            log.info(this, "jdbc log is is configured to use a database, using System.out instead");
        }
        try
        {
            loadJDBCDriver(log);
            connectToDB();
            createTableIfNeeded(log);
            addRow(level, formatClassName(obj), m_name, message, formatException(except));
            log.info(this, "Inserted log message into db");
        }
        catch(NoSuchFieldException e)
        {
            log.error(this, "JDBC Error: " + e.getMessage(), e);
        }
        catch(ClassNotFoundException e)
        {
            log.error(this, "JDBC driver not found", e);
        }
        catch(SQLException e)
        {
            m_tableCreated = false;
            log.error(this, "JDBC Error: " + e.getMessage() + " (code=" + e.getErrorCode() + ")", e);
        }
    }

    private void loadJDBCDriver(LogPrinter log)
        throws NoSuchFieldException, ClassNotFoundException
    {
        if(m_driverLoaded)
            return;
        String driver = Config.getConfig().get("log/jdbc/driver", "");
        if(driver.length() == 0)
        {
            throw new NoSuchFieldException("No JDBC driver configured (log/jdbc/driver)");
        } else
        {
            log.info(this, "Loading driver " + driver);
            Class.forName(driver);
            m_driverLoaded = true;
            return;
        }
    }

    private void connectToDB()
        throws SQLException
    {
        if(m_conn != null)
        {
            return;
        } else
        {
            String user = Config.getConfig().get("log/jdbc/username", "system");
            String pass = Config.getConfig().get("log/jdbc/password", "");
            m_conn = DriverManager.getConnection(m_dsn, user, pass);
            return;
        }
    }

    private void createTableIfNeeded(LogPrinter log)
    {
        if(m_tableCreated)
            return;
        try
        {
            Statement stmt = m_conn.createStatement();
            stmt.executeUpdate("create table LogMessage (ID          integer NOT NULL,OBJ_NAME    varchar (32),LOG_NAME    varchar (32),LOG_LEVEL   integer NOT NULL,TEXT        varchar (255),LOG_DATE    datetime,HOSTNAME    varchar (80),THREAD      varchar (16),EXCEPTION   varchar (255))");
            stmt.close();
            m_tableCreated = true;
            log.info(this, "Created LogMessage table");
        }
        catch(SQLException e)
        {
            m_tableCreated = true;
            log.info(this, "Failed to create LogMessage table: " + e.getMessage());
        }
    }

    private void addRow(int level, String obj, String name, String message, String exception)
        throws SQLException
    {
        String update = "insert into LogMessage values (?,?,?,?,?,?,?,?,?)";
        PreparedStatement stmt = m_conn.prepareStatement(update);
        int id = 0;
        synchronized(this)
        {
            id = ++uniqueNumber;
        }
        stmt.setInt(1, id);
        stmt.setString(2, obj);
        stmt.setString(3, name);
        stmt.setInt(4, level);
        stmt.setString(5, message);
        stmt.setTimestamp(6, new Timestamp(System.currentTimeMillis()));
        stmt.setString(7, getHostName());
        stmt.setString(8, Thread.currentThread().getName());
        stmt.setString(9, exception);
        stmt.executeUpdate();
        stmt.close();
    }

    private String m_name;
    private boolean m_configuredOn;
    private int m_configuredLevel;
    private String m_fileName;
    private String m_dsn;
    private PrintStream m_stream;
    private boolean m_driverLoaded;
    private boolean m_tableCreated;
    private Connection m_conn;
    private DateFormat m_datefmt;
    private SimpleDateFormat m_simpdatefmt;
    private static final int NOT_FOUND = -1;
    private static final String filePrefix = "file:";
    private static final String odbcPrefix = "jdbc:";
    static boolean m_testbedApplication = System.getProperty("TestbedApplication", "0").equals("1");
    private static int uniqueNumber = 0;

}